using System;   
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SqlServer.Types;

namespace Storage
{
    using Exceptions;
    using EntityModels;

    #region StorageManager
   
    /// <summary>
    /// Rapresents the way in which the modules can handle and persist their own data.
    /// </summary>
    public sealed partial class StorageManager
    {
        #region Public methods

        #region GetUsersWithReservationRequestByTripId

        /// <summary>
        /// Returns the id's of all users that have made a request for a certain trip.
        /// </summary>
        /// <param name="tripId">The trip's id.</param>
        /// <returns>A list of user id.</returns>
        public List<int> GetUsersWithReservationRequestByTripId(int tripId)
        {
            return GetTripById(tripId).ReservationRequests.Select(r => r.UserId).Distinct().ToList();
        }

    	#endregion

	    #region GetUsersWithReservationRequestByTrip

        /// <summary>
        /// Returns the id's of all users that have made a request for a certain trip.
        /// </summary>
        /// <param name="trip">The trip.</param>
        /// <returns>A list of user id.</returns>
        public List<int> GetUsersWithReservationRequestByTrip(Trip trip)
        {
            return GetUsersWithReservationRequestByTripId(trip.Id);
        }

        #endregion

        #region GetUserById

        /// <summary>
        /// Returns the user given his Id.
        /// </summary>
        /// <param name="userId">The user's id.</param>
        /// <returns>The user associated with the userId.</returns>
        public User GetUserById(int userId)
        {
            try
            {
                return Entities.Users.First(user => user.Id == userId);
            }
            catch
            {
                throw new UserNotFoundException(userId);
            }
        }

        #endregion

        #region GetRelation

        /// <summary>
        /// Returns the relation that exist between two users. 
        /// </summary>
        /// <param name="userID1">The id of the first User.</param>
        /// <param name="userID2">The id of the second User.</param>
        /// <returns>Returns the PrivacyLevel of the relation.</returns>
        public PrivacyLevel GetRelation(int userID1, int userID2)
        {
            // if a relation is contained into the table TempRelationship the PrivacyLevel between
            // the specified users is returned.
            PrivacyLevel result;
            if (Entities.TempRelationships.TryGetValue(new KeyValuePair<int, int>(userID1, userID2), out result))
            {
                return result;
            }
            else
            {
                User user1 = GetUserById(userID1);
                User user2 = GetUserById(userID2);
            
                // if a user is contained into the BuddyList of another user Low PrivacyLevel is returned.
                if (Entities.BuddyLists
                    .Any(f => f.SenderUserId == user1.Id && f.ReceiverUserId == user2.Id && !f.IsBanned ||
                              f.SenderUserId == user2.Id && f.ReceiverUserId == user1.Id && !f.IsBanned))
                {
                    return PrivacyLevel.Low;
                }
                else
                {
                    // gets the stages in which user1 has joined.
                    var relation2 = from user_stage in Entities.Users_Stages
                                    where user_stage.UserId == user1.Id
                                    select user_stage.Stage;

                    if (relation2.Any(s => s.DriverId == user2.Id))
                        return PrivacyLevel.Medium;

                    // gets the stages in which user2 has joined.
                    var relation3 = from user_stage in Entities.Users_Stages
                                    where user_stage.User.Id == user2.Id
                                    select user_stage.Stage;

                    if (relation3.Any(s => s.DriverId == user1.Id) || 
                        relation3.Intersect(relation2).Any())
                        return PrivacyLevel.Medium;
                }

                // The two users have no relation.
                return PrivacyLevel.High;
            }
        }       

        #endregion

        #region SetRelation

        /// <summary>
        /// Set the relation between two users.
        /// </summary>
        /// <param name="userID1">The id of the first User.</param>
        /// <param name="userID2">The id of the second User.</param>
        /// <param name="level">The PrivacyLevel of the relation.</param>
        public void SetRelation(int userID1, int userID2, PrivacyLevel level)
        {
            // adds a new temporary relation to _tempRelationship table.
            Entities.TempRelationships.Add(new KeyValuePair<int, int>(userID1, userID2), level);
        }

        #endregion

        #region GetFixedPrivacyLevels

        /// <summary>
        /// Returns the FixedPrivacyLevels of TripLoa user.
        /// </summary>
        public FixedPrivacyLevels GetFixedPrivacyLevels()
        {
            return Entities.FixedPrivacyLevels.First();
        }

        #endregion

        #region GetUserByUserName

        /// <summary>
        /// Gets a User given his username.
        /// </summary>
        /// <param name="username">The user's username.</param>
        /// <returns>The User with the given username.</returns>
        public User GetUserByUserName(string username)
        {
            var query = from u in Entities.Users
                        where u.Username == username
                        select u;
            try
            {
                return query.First();
            }
            catch (InvalidOperationException)
            {
                throw new UserNotFoundException(username);
            }
        }

        #endregion

        #region GetUserIdByUserName

        /// <summary>
        /// Gets the Id of a User given his username.
        /// </summary>
        /// <param name="username">The user's username.</param>
        /// <returns>The user's Id.</returns>
        public int GetUserIdByUserName(string username)
        {
            return GetUserByUserName(username).Id;
        }

        #endregion

        #region GetFriendsByUserId

        /// <summary>
        /// Returns the id's of all friends of a user.
        /// </summary>
        /// <param name="userId">The user's id from which to get the friends.</param>
        /// <returns>A List of id's of friends.</returns>
        public List<int> GetFriendsByUserId(int userId)
        {
            // NB: This line is only used to check if the user exists.
            User user = GetUserById(userId);

            var query = from friend in Entities.BuddyLists
                        where friend.SenderUserId == userId && !friend.IsBanned
                        select friend.ReceiverUserId;

            return query.ToList();
        }

        #endregion

        #region GetBannedFriendsByUserId

        /// <summary>
        /// Returns the id's of all banned friends of a user.
        /// </summary>
        /// <returns>A List of id's of banned friends.</returns>
        public List<int> GetBannedFriendsByUserId(int userId)
        {
            // NB: This line is only used to check if the user exists.
            User user = GetUserById(userId);

            var query = from friend in Entities.BuddyLists
                        where friend.SenderUserId == userId && friend.IsBanned
                        select friend.ReceiverUserId;

            return query.ToList();
        }

        #endregion

        #region GetFriendsIdFromSN

        /// <summary>
        /// Returns a list of Ids, one for each friend of "this" with account on both "social_networks" and TripLOA.
        /// </summary>
        /// <param name="social_network">The social network from which to get the users.</param>
        /// <param name="friends">A list of friends.</param>
        /// <returns>A list of id's.</returns>
        public List<int> GetFriendsIdFromSN(string social_network, List<string> friends)
        {
            var db = StorageManager.Instance.Entities;

            var query = from account in db.SocialNetworkSubscribeDetails
                        where account.SocialNetwork.Name == social_network && friends.Contains(account.UserToken)
                        select account.UserId;

            return query.ToList();
        }

        /// <summary>
        /// Returns a list of Ids, one for each friend of "this" with account on both "social_networks" and TripLOA.
        /// </summary>
        /// <param name="userId">The user's Id.</param>
        /// <param name="social_network">The social network from which to get the users.</param>
        /// <param name="friends">A list of friends.</param>
        /// <returns>A list of id's.</returns>
        public List<int> GetFriendsIdFromSNExcludingBuddies(string social_network, List<string> friends, int userId)
        {
            //Implemented by Licciardi & the Interopers
            var db = StorageManager.Instance.Entities;

            var currUser = (from u in db.Users 
                            where u.Id == userId 
                            select u).First();

            var userBuddies = from bud in db.BuddyLists 
                              where bud.Buddy == currUser 
                              select bud.OwnerUser;

            var snFriends = from account in db.SocialNetworkSubscribeDetails
                            where account.SocialNetwork.Name == social_network && friends.Contains(account.UserToken)
                            select account.User;

            var result = from r in snFriends.Except(userBuddies) 
                         select r.Id;
            
            return result.ToList();
        }

        #endregion

        #region GetPrivacyLevels

        /// <summary>
        /// Gets the privacy levels associated to a user.
        /// </summary>
        /// <param name="userId">The user's Id.</param>
        /// <returns>
        /// A Dictionary that consists of pairs: 
        /// field's name and the related privacy level.
        /// </returns>
        public Dictionary<string, PrivacyLevel> GetPrivacyLevels(int userId)
        {
            User user = GetUserById(userId);
            
            var variablePrivacyLevels = user.VariablePrivacyLevels;
            var fixedPrivacyLevels = GetFixedPrivacyLevels();
            
            Dictionary<string, PrivacyLevel> result = new Dictionary<string, PrivacyLevel>();

            // Fields that have a variable privacy level.
            result.Add("Name", (PrivacyLevel)variablePrivacyLevels.Name);
            result.Add("Surname", (PrivacyLevel)variablePrivacyLevels.Surname);
            result.Add("City", (PrivacyLevel)variablePrivacyLevels.City);
            result.Add("Address", (PrivacyLevel) variablePrivacyLevels.Address);
            result.Add("Interests", (PrivacyLevel)variablePrivacyLevels.Interests);
            result.Add("Birthday", (PrivacyLevel)variablePrivacyLevels.Birthday);
            result.Add("PhoneNumber", (PrivacyLevel)variablePrivacyLevels.PhoneNumber);
            result.Add("SocialNetworks", (PrivacyLevel)variablePrivacyLevels.SocialNetworks);
            result.Add("BuddyLists", (PrivacyLevel)variablePrivacyLevels.BuddyLists);
            result.Add("Vehicles", (PrivacyLevel)variablePrivacyLevels.Vehicles);
            result.Add("AvatarPath", (PrivacyLevel)variablePrivacyLevels.AvatarPath);

            // Fields that have a fixed privacy level.
            result.Add("Age", (PrivacyLevel) fixedPrivacyLevels.Age);            
            result.Add("DrivingLicence", (PrivacyLevel) fixedPrivacyLevels.DrivingLicence);
            result.Add("Email", (PrivacyLevel) fixedPrivacyLevels.Email);
            result.Add("Gender", (PrivacyLevel) fixedPrivacyLevels.Gender);
            result.Add("Username", (PrivacyLevel)fixedPrivacyLevels.Username);
            result.Add("Smoker", (PrivacyLevel) fixedPrivacyLevels.Smoker);
            result.Add("Feedbacks", (PrivacyLevel)fixedPrivacyLevels.Feedbacks);
            
            return result;
        }

        #endregion

        #region SetPrivacyLevels

        /// <summary>
        /// Sets the privacy levels of a user.
        /// </summary>
        /// <param name="userId">The user's id.</param>
        /// <param name="fieldPrivacyLevels">The new set of privacy levels.</param>
        public void SetPrivacyLevels(int userId, Dictionary<string, PrivacyLevel> fieldPrivacyLevels)
        {
            foreach (string fieldName in fieldPrivacyLevels.Keys)
            {
                SetSinglePrivacyLevel(userId, fieldName, fieldPrivacyLevels[fieldName]);
            }
        }

        #endregion

        #region SetSinglePrivacyLevel
        
        /// <summary>
        /// Sets a privacy level of a fiels for a certain user.
        /// </summary>
        /// <param name="userId">The user'id.</param>
        /// <param name="fieldName">The field's name.</param>
        /// <param name="fieldPrivacyLevel">The new privacy level.</param>
        public void SetSinglePrivacyLevel(int userId, string fieldName, PrivacyLevel fieldPrivacyLevel)
        {
            User user = GetUserById(userId);
            var variablePrivacyLevels = user.VariablePrivacyLevels;

            // NB: check if the field that we're modifying is one with variabile privacy level.
            if (!variablePrivacyLevels.GetType().GetProperties().Select(p => p.Name).Contains(fieldName))
                throw new ArgumentException("The field is a fixed privacy level one. Don't try to modify its privacy level!");

            variablePrivacyLevels.GetType().GetProperty(fieldName).SetValue(variablePrivacyLevels, fieldPrivacyLevel, null);
        }

        #endregion
        
        #region AddFriend

        /// <summary>
        /// Adds a new friend to the buddy list of User with Id = userID.
        /// </summary>
        /// <param name="userID">The user owner of the friendship relation.</param>
        /// <param name="friendID">The id of the user to make friend.</param>
        public void AddFriend(int userID, int friendID)
        {
            User user = GetUserById(userID);

            user.BuddyList.Add(new BuddyList
            {
                OwnerUser = user,
                ReceiverUserId = friendID
            });
            
            // Add the simmetrical friendship relation.
            User friend = GetUserById(friendID);
            friend.BuddyList.Add(new BuddyList
            {
                OwnerUser = friend,
                ReceiverUserId = userID
            });

            Entities.SubmitChanges();
        }

        /// <summary>
        /// Adds some friends to the buddy list of User with Id = userID.
        /// </summary>
        /// <param name="userId">The user owner of the friendship relation.</param>
        /// <param name="friendIds">The id of the user to make friend.</param>
        public void AddFriend(int userId, IEnumerable<int> friendIds)
        {
            User user = GetUserById(userId);

            foreach (var friendId in friendIds)
            {
                user.BuddyList.Add(new BuddyList
                {
                    OwnerUser = user,
                    ReceiverUserId = friendId
                });

                // Add the simmetrical friendship relation.
                User friend = GetUserById(friendId);
                friend.BuddyList.Add(new BuddyList
                {
                    OwnerUser = friend,
                    ReceiverUserId = userId
                });
            }

            Entities.SubmitChanges();
        }

        #endregion

        #region RemoveFriend

        /// <summary>
        /// Removes a friend from the buddy list.
        /// </summary>
        /// <param name="userId"></param>
        /// <param name="friendId"></param>
        public void RemoveFriend(int userId, int friendId)
        {
            User user = GetUserById(userId);

            var toDelete = Entities.BuddyLists
                .Single(buddy => buddy.OwnerUser == user && buddy.ReceiverUserId == friendId);
            
            Entities.BuddyLists.DeleteOnSubmit(toDelete);

            // Remove the simmetrical friendship relation.
            User friend = GetUserById(friendId);
            var toDelete2 = Entities.BuddyLists
                .Single(buddy => buddy.OwnerUser == friend && buddy.ReceiverUserId == userId);
            
            Entities.BuddyLists.DeleteOnSubmit(toDelete2);

            Entities.SubmitChanges();
        }

        #endregion

        #region GetBannedUsersByUserId
        /// <summary>
        /// gets ids of users banned by user
        /// </summary>
        /// <param name="userId">id of user</param>
        /// <returns>list of users banned</returns>
        public List<int> GetBannedUsersByUserId(int userId)
        {
            User user = GetUserById(userId);

            var query = from banned in Entities.BlackLists
                        where banned.MasterUserId == userId
                        select banned.BannedUserId;

            return query.ToList();
        }

        #endregion GetBannedUsersByUserId

        #region UnbanUser

        /// <summary>
        /// unbans a user
        /// </summary>
        /// <param name="userId">user id who is unbanning</param>
        /// <param name="otherUserId">unbanned user id</param>
        public void UnbanUser(int userId, int otherUserId)
        {
            User user = GetUserById(userId);

            var toDelete =
                Entities.BlackLists
                .Single(banned => banned.MasterUserId == userId &&
                                 banned.BannedUserId == otherUserId);

            Entities.BlackLists.DeleteOnSubmit(toDelete);

            Entities.SubmitChanges();
        }

        #endregion UnbanUser

        #region BanUser

        /// <summary>
        /// bans a specified user
        /// </summary>
        /// <param name="userId">user who is banning</param>
        /// <param name="otherUserId">id of user banned</param>
        public void BanUser(int userId, int otherUserId)
        {
            User user = GetUserById(userId);

            // check if already exist
            var query = from banned in Entities.BlackLists
                        where banned.MasterUserId == userId && banned.BannedUserId == otherUserId
                        select banned.BannedUserId;

            List<int> tmp = query.ToList();

            // entry not found => add
            if (tmp.Count == 0)
                user.BlackLists.Add(new BlackList { MasterUserId = user.Id, BannedUserId = otherUserId });


            // FIX also delete otherUserId if they are friends _ONLY_ (userId,otherUserId) ?

            Entities.SubmitChanges();
        }

        #endregion BanUser

        #region UnbanFriend

        /// <summary>
        /// Unbans a friend.
        /// </summary>
        /// <param name="userId">The user owner of the friendship relation.</param>
        /// <param name="friendId">The friend's id to unban.</param>
        public void UnbanFriend(int userId, int friendId)
        {
            User user = GetUserById(userId);

            var buddyListEntry =
                Entities.BuddyLists
                .First(buddy => buddy.OwnerUser == user && buddy.ReceiverUserId == friendId);

            buddyListEntry.IsBanned = false;
            Entities.SubmitChanges();
        }

        #endregion

        #region BanFriend

        /// <summary>
        /// Bans a friend.
        /// </summary>
        /// <param name="userId">The user owner of the friendship relation.</param>
        /// <param name="friendId">The friend's id to ban.</param>
        public void BanFriend(int userId, int friendId)
        {
            User user = GetUserById(userId);

            var buddyListEntry =
                Entities.BuddyLists
                .First(buddy => buddy.OwnerUser == user && buddy.ReceiverUserId == friendId);

            buddyListEntry.IsBanned = true;
            Entities.SubmitChanges();
        }

        #endregion

        #endregion
    }
 
    #endregion
}
